﻿/*
BF4Rcon.NET, provides a .NET implementation of BF4's RCON interface.
Copyright (C) 2013 Tim Marro

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TimScripts.BF4Rcon.Frostbite.Resources;

namespace TimScripts.BF4Rcon.Frostbite
{
    public class RconClient : IRconClient, IRconEventProvider, IDisposable
    {
        private const int MaxSequenceValue = Packet.MaxSequenceValue;
        private IRconConnection<Packet> Connection;
        private int SequenceNumber = 0;
        private CancellationTokenSource DisconnectionCancellationTokenSource;
        private bool IsSetup = false;
        private object SetupSynchronizationObject = new object();
        private bool Disposed = false;
        private IRequestManager RequestManager;
        private Exception UnderlyingDisconnectionException;

        public event EventHandler<RconEventArgs> RconEvent;

        public RconClient(IRconConnection<Packet> connection)
        {
            if (connection == null)
                throw new ArgumentNullException("connection");

            Connection = connection;

            if (connection.IsConnected)
                Setup();
        }

        public IList<string> SendRequest(IEnumerable<string> words)
        {
            return SendRequest(words, Timeout.Infinite);
        }

        public IList<string> SendRequest(IEnumerable<string> words, int timeout)
        {
            Packet request;
            Packet response;
            int sequence;

            CheckConnectionStatus();

            if (!Connection.IsConnected) throw CreateDisconnectionException();

            request = GetRequestPacket(words);
            sequence = request.Sequence;

            Connection.SendPacket(request);

            response = RequestManager.GetResponse(sequence, timeout);

            return response.Words;
        }

        private DisconnectionException CreateDisconnectionException()
        {
            DisconnectionException result;

            if (UnderlyingDisconnectionException != null)
                result = new DisconnectionException(PortableErrorStrings.Disconnection_Error, UnderlyingDisconnectionException);
            else
                result = new DisconnectionException();

            return result;
        }

        public async Task<IList<string>> SendRequestAsync(IEnumerable<string> words, CancellationToken ct)
        {
            Packet request;
            Packet response;

            CheckConnectionStatus();

            if (!Connection.IsConnected) throw CreateDisconnectionException();

            request = GetRequestPacket(words);

            await Connection.SendPacketAsync(request).ConfigureAwait(false);

            response = await RequestManager.GetResponseAsync(request.Sequence, ct);

            return response.Words;
        }

        public IList<string> SendRequest(params string[] words)
        {
            return SendRequest((IEnumerable<string>)words);
        }

        public IList<string> SendRequest(int timeout, params string[] words)
        {
            return SendRequest(words, timeout);
        }

        public Task<IList<string>> SendRequestAsync(IEnumerable<string> words)
        {
            return SendRequestAsync(words, CancellationToken.None);
        }

        public Task<IList<string>> SendRequestAsync(params string[] words)
        {
            return SendRequestAsync((IEnumerable<string>)words);
        }

        public Task<IList<string>> SendRequestAsync(CancellationToken ct, params string[] words)
        {
            return SendRequestAsync((IEnumerable<string>)words, ct);
        }

        public bool IsLoggedOn
        {
            get;

            private set;
        }

        public bool LogOn(string password)
        {
            IList<string> response;
            string salt;
            string passwordHash;

            if (string.IsNullOrEmpty(password))
                throw new ArgumentNullException("password");

            response = SendRequest("login.hashed");

            if (response[0].Equals("PasswordNotSet"))
                throw new PasswordNotSetException();

            salt = response[1];

            passwordHash = GeneratePasswordHash(password, salt);

            response = SendRequest("login.hashed", passwordHash);

            if (response[0].Equals("InvalidPasswordHash"))
                throw new IncorrectPasswordException();

            return IsLoggedOn = response[0].Equals("OK");
        }

        private static string GeneratePasswordHash(string password, string salt)
        {
            int saltByteLen = salt.Length / 2;
            byte[] passBytes = Encoding.UTF8.GetBytes(password);
            byte[] allBytes = new byte[passBytes.Length + saltByteLen];
            string result;

            for (int i = 0; i < saltByteLen; i++)
            {
                allBytes[i] = Convert.ToByte(salt.Substring(2 * i, 2), 16);
            }

            Buffer.BlockCopy(passBytes, 0, allBytes, saltByteLen, passBytes.Length);

            result = MD5.GenerateHash(allBytes);

            return result.ToUpperInvariant();
        }

        public void Connect()
        {
            //consider waiting for cleanup
            if (Connection.IsConnected)
                throw new InvalidOperationException(PortableErrorStrings.InvalidOperation_AlreadyConnected);

            Connection.Connect();

            //TODO: client specific stuffs
            Setup();
        }

        public void Disconnect()
        {
            if (!Connection.IsConnected) return;

            //TODO: client-specific stuff
            //prob just clean up most things after readloop
            CleanUp(new DisconnectionException());

            //disconnect as late as possible for threads still active
            Connection.Disconnect();
        }

        public virtual void OnEvent(RconEventArgs args)
        {
            if (RconEvent != null)
                RconEvent(this, args);
        }

        private void OnEvent(Packet packet)
        {
            var packetWords = packet.Words;
            string eventName = packetWords[0];

            List<string> eventData = new List<string>(packetWords.Skip(1));

            RconEventArgs args = new RconEventArgs(eventName, new ReadOnlyCollection<string>(eventData));

            ThreadPool.QueueUserWorkItem((state) =>
            {
                OnEvent(args);
            });
        }

        private void Setup()
        {
            //consider making the interface implement idisposable
            IDisposable rm = RequestManager as IDisposable;

            lock (SetupSynchronizationObject)
            {
                IsSetup = true;
                UnderlyingDisconnectionException = null;
            }

            if (rm != null)
            {
                rm.Dispose();
            }
            RequestManager = new RequestManager();
            DisconnectionCancellationTokenSource = new CancellationTokenSource();
            BeginThreadPoolLoop(DisconnectionCancellationTokenSource.Token);
        }

        //private void CleanUp()
        //{
        //    CleanUp(null);
        //}

        private void CleanUp(Exception exception)
        {
            if (DisconnectionCancellationTokenSource != null)
            {
                DisconnectionCancellationTokenSource.Cancel();
                DisconnectionCancellationTokenSource.Dispose();
                DisconnectionCancellationTokenSource = null;
            }

            RequestManager.Close(exception);
            SequenceNumber = 0;

            lock (SetupSynchronizationObject)
            {
                if (!(exception is DisconnectionException) && UnderlyingDisconnectionException == null)
                    UnderlyingDisconnectionException = exception;

                IsSetup = false;
            }
        }

        //using this kind of loop in order to not lock down a single threadpool thread the entire time
        private void BeginThreadPoolLoop(CancellationToken ct)
        {
            ThreadPool.QueueUserWorkItem((state) => ThreadPoolLoop(state), ct);
        }

        private async Task ThreadPoolLoop(object state) //ct
        {
            CancellationToken ct = (CancellationToken)state;
            await DoRead();
            //TODO: pass cancellationtoken to doread to cancel on receive (ornot idk yet)
            if (ct.IsCancellationRequested) return;

            BeginThreadPoolLoop(ct);

            return;
        }

        private async Task DoRead()
        {
            Packet packet;

            try
            {
                packet = await Connection.ReceivePacketAsync().ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                CleanUp(ex);
                return;
            }

            if (packet.IsResponse)
            {
                RequestManager.AddResponse(packet);
            }
            else //clearly not from client if we receiving from the server
            {
                OnEvent(packet);
            }
        }

        private Packet GetRequestPacket(IEnumerable<string> words)
        {
            Packet request;
            int seq = Interlocked.Increment(ref SequenceNumber);

            //enforce maximum
            Interlocked.CompareExchange(ref SequenceNumber, 0, MaxSequenceValue);

            request = new Packet(true, false, seq, words);

            return request;
        }

        private void CheckConnectionStatus()
        {
            lock (SetupSynchronizationObject)
            {
                if (!IsSetup && Connection.IsConnected)
                {
                    Setup();
                }
                else if (IsSetup && !Connection.IsConnected)
                {
                    CleanUp(new DisconnectionException());
                }
            }
        }


        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!Disposed)
            {
                if (disposing)
                {
                    CleanUp(new DisconnectionException());
                    Disposed = true;
                }
            }
        }
    }
}
